import java.util.*;
import java.lang.Math;
import java.text.DecimalFormat;
import Jama.*;

public class CurveFitting {
    //int degree = 4;
    final int maxDegree = 15;
    private int degree = 15;
    private int highIn = Integer.MIN_VALUE;
    private int lowIn = Integer.MAX_VALUE;
    private Matrix X,Y,a, inputs;
    private int periodicity = -1;    // is != -1 if function is periodic
    //ArrayList<Double> uniqueInputs;

    public CurveFitting(ArrayList<Tuple> allEx){
        inputs = getInOrOut(allEx,1);
        //uniqueInputs = getUniqueInputs(inputs);
        degree = getDegree(inputs, this.maxDegree);
        getHighLowIn (inputs);    // gets highest and lowest input
        Y = getYMatrix(allEx);
        X = getXMatrix(allEx);
        a = findCoefficients();
        findExtrapolation();
        //Y.print(4, 0);
        //X.print(4, 0);
        //a.print(4, 0);

        System.out.println(this.print());
        System.out.println(periodicity);
        System.out.println("==== genereted function start ====");
        System.out.println(printJava());
        System.out.println("==== genereted function stop ====");
    }

    public String print(){
        int nbrCoff = a.getRowDimension();
        String s = "y = ";
        String sign = "";
        String coff = "";
        DecimalFormat format = new DecimalFormat();
        format.setMinimumIntegerDigits(1);
        format.setMaximumFractionDigits(10);
        format.setMinimumFractionDigits(0);

        for(int i = 0; i < nbrCoff ; i++){
            if(a.get(i, 0) < 0)
                sign = " - ";
            else if(a.get(i, 0) > 0)
                sign = " + ";

            coff = format.format(Math.abs(a.get(i,0)));
            s += sign + coff;
            
            if(i == 1)
                s += "x";
            else if(i > 0)
                s += "x^" + i;
        }
        return s;
    }

    public String printJava(){
        String function = "public static int generatedFunction(int x){\n";
        if(periodicity != -1){
            function += "\tint periodicity = " + periodicity + ";\n";
            function += "\tint start = " + lowIn + ";\n";
            function += "\tif((start-x) % periodicity == 0)\n";
            function += "\t\tx = start;\n";
            function += "\telse if(x > start)\n";
            function += "\t\tx = start + (x-start) % periodicity;\n";
            function += "\telse\n";
            function += "\t\tx = start + periodicity - (start - x) % periodicity;\n";
        }
        function += "\tdouble a[] = {\n";
        for(int i = 0 ; i < a.getRowDimension(); i++){
            function += "\t\t" + a.get(i, 0);
            if(i != a.getRowDimension()-1)
                function += ",";
            function += "\n";
        }
        function += "\t};\n";
        function += "\tdouble tempY = 0;\n";
        function += "\tint y = 0;\n";
        function += "\tint nbrCoff = a.length;\n";
        function += "\n";
        function += "\tfor(int i = 0; i < nbrCoff; i++){\n";
        function += "\t\ttempY += a[i] * Math.pow(x, i);\n";
        function += "\t}\n";
        function += "\n";
        function += "\ty = (int)Math.round(tempY);";
        function += "\n";
        function += "\treturn y;\n";

        function += "}";
        return function;
    }

    private Matrix findCoefficients(){
        // a = (X^T * X)^-1 * X^T * Y
        return (X.transpose().times(X)).inverse().times(X.transpose()).times(Y);
    }

    private void findExtrapolation(){
        isRepeating();  // tries to figure out if function has periodicity
    }

    private void isRepeating(){
        if(degree < 5)  // cant tell if repeating
            return;
        
        int length = this.highIn - this.lowIn;
        int middle = this.lowIn + (length / 2) + (length % 2);

        for(int i = this.lowIn + 1 ;i <= middle; i++){
            length--;
            for(int j = 0 ; j <= length; j++){
                if(generatedFunction(i + j) != generatedFunction(this.lowIn +j))
                    break;
                else if(j == length){
                    periodicity = i - this.lowIn;
                    return;
                }
            }
            
        }

        generatedFunction(5);
        //periodicity = 5;
    }

    private int generatedFunction(int x){
        double tempY = 0;
        int y = 0;
        int nbrCoff = a.getRowDimension();

        for(int i = 0; i < nbrCoff; i++){
            tempY += a.get(i, 0) * Math.pow(x, i);
        }

        y = (int) tempY;

        if((tempY - y) >= 0.5)  // for propper rounding
            y++;

        return y;
    }

    private void  getHighLowIn (Matrix inputs){
        int numRows = inputs.getRowDimension();

        for(int i = 0;i < numRows; i++){
            if(inputs.get(i, 0) > this.highIn)
                this.highIn = (int)inputs.get(i, 0);
            if(inputs.get(i, 0) < this.lowIn)
                this.lowIn = (int)inputs.get(i, 0);
        }
    }

    /*private ArrayList<Double> getUniqueInputs(Matrix inputs){
        int numRows = inputs.getRowDimension();
        ArrayList<Double> tempX = new ArrayList<Double>();

        for(int i = 0; i < numRows; i++){
            if(tempX.indexOf(inputs.get(i, 0)) == -1){
                tempX.add(inputs.get(i, 0));
            }
        }
        return tempX;
    }*/

    private int getDegree(Matrix inputs, int maxDegree){
        int numRows = inputs.getRowDimension();
        ArrayList<Double> tempX = new ArrayList<Double>();

        for(int i = 0; i < numRows; i++){
            if(tempX.indexOf(inputs.get(i, 0)) == -1){
                tempX.add(inputs.get(i, 0));
            }
        }

        if(tempX.size() < maxDegree)
            return tempX.size();
        else
            return maxDegree;
    }

    private Matrix getXMatrix(ArrayList<Tuple> allEx){
        int nbrRows = this.inputs.getRowDimension();
        int nbrCol = this.degree;
        Matrix XMatrix = new Matrix(nbrRows, nbrCol);
        double tempX;
        
        for(int i = 0; i < nbrRows; i++){
            tempX = this.inputs.get(i, 0);
            for(int j = 0; j <nbrCol; j++){
                XMatrix.set(i, j,Math.pow(tempX,j) );
            }
        }
        
        return XMatrix;
    }

    private Matrix getYMatrix(ArrayList<Tuple> allEx){
        return getInOrOut(allEx,2);
    }

    private Matrix getInOrOut(ArrayList<Tuple> allEx, int inOrOut){
        double [] col = {};
        double [] tempCol;
        double [] tempCol2;

        //ArrayList<Double> outputs = new ArrayList<Double>();
        int nbrEx = allEx.size();
        int nbrOut;

        // puts all outputs in allEx in outputs(casted to double)
        for (int i = 0; i < nbrEx; i++){
            nbrOut = allEx.get(i).get(inOrOut).size();
            tempCol = new double[nbrOut];
            for(int j = 0; j < nbrOut; j++){
                //outputs.add((double)allEx.get(i).get(2).get(j));
                tempCol[j] = (double)allEx.get(i).get(inOrOut).get(j);
            }
            tempCol2 = col.clone();
            col = new double[col.length + tempCol.length];
            System.arraycopy(tempCol2, 0, col, 0, tempCol2.length);
            System.arraycopy(tempCol, 0, col, tempCol2.length, tempCol.length);
        }

        double[][] mat = {col};

        return new Matrix(mat).transpose();
    }
}
